ARTICLES: IPX/SPX for NetBIOS Developers Original article: (c) Copyright Novell, 1994 Novell Professional Developer BULLETS January 1994 (Volume 6, Number 1) NwTP additions : (between angular brackets/ minus signs [- ... -]) NetBIOS is a popular peer-to-peer communication method that it is supported under NetWare through a NetBIOS emulator. However, even though NetBIOS is supported, there are definite advantages to using Novell's "native tongue" protocols, IPX (Internet Packet eXchange) and SPX (Sequenced Packet eXchange), when doing peer-to-peer communication. This article discusses the advantages of using IPX/SPX and provides an introduction to Novell's IPX and SPX protocols for developers who have a working familiarity with NetBIOS. Why Use IPX/SPX? The most obvious reason to use IPX and SPX is to improve performance; since NetWare emulates NetBIOS, processing NetBIOS commands involves more overhead than processing IPX/SPX commands. NetWare encapsulates emulated NetBIOS packets within IPX packets before they go out on the wire, so moving to IPX/SPX allows you to "cut out the middleman." You lose no connectivity by switching protocols either, since the emulated NetBIOS layer cannot communicate with hardware NetBIOS systems. In fact, moving to IPX/SPX gives you a net gain in connectivity; NetWare has a 70% share of the network operating system market. Also, since the NetBIOS emulator adds an additional layer of complexity to packets being sent out, it is more difficult to troubleshoot problems. Emulating NetBIOS involves an extra driver and an extra set of potential incompatibilities. Generally speaking, since IPX and SPX are not dramatically different from NetBIOS, it makes your job easier to work with the protocols that NetWare is designed to support. Datagram Services Novell's IPX protocol provides almost the same functionality as NetBIOS datagrams. Both specifications deliver packets on a best-effort basis, but with no guarantee of delivery or sequencing. Both IPX and NetBIOS also provide the capability to send packets either to a single node or to multiple nodes. NetBIOS supports the multicast, or the sending of a datagram to a selected group of nodes with the same group name. Since IPX is address-based instead of name-based, this capability is not directly supported; instead IPX must send an individual packet to each node. NetBIOS also supports the broadcast datagram, a datagram that is broadcast to the entire internetwork. IPX supports broadcasts, but only to one subnet at a time. Usually, this restriction poses no problem, since mechanisms such as the NetWare Service Advertising Protocol (SAP) overcome this limitation. The data portion of a NetBIOS datagram is limited in length to 512 bytes, whereas IPX packets allow 546 bytes of data on all networks and can sometimes be substantially larger than that depending on the maximum packet size supported by network routers. Some networks can handle packet sizes of 4096 bytes or more. Session Services As in the relationship between IPX and NetBIOS datagrams, Novell's SPX protocol serves much the same function as the NetBIOS session. Both SPX and NetBIOS sessions provide guaranteed delivery and sequencing of packets, but at the cost of increased overhead. The primary difference between the two is the supported packet size. NetBIOS sessions support 64K packet sizes (128K with Chain Sends). SPX has the same 546-byte packet size limitation as IPX and, in fact, SPX allows slightly less data in a packet than IPX, since the SPX header requires an additional 12 bytes. SPX therefore supports 534 bytes of data on all networks with the potential for much larger packets if supported by the routers, although attaining a 64K packet size is unlikely. Probably the most noticeable difference between IPX/SPX and NetBIOS is how each addresses packets. IPX/SPX addresses packets using network, node, and socket numbers. NetBIOS uses unique names to address packets. Each workstation can be uniquely addressed using the network and node numbers. A workstation can then have as many open sockets as desired for receiving peer-to-peer data packets. Many methods exist for determining a workstation's network, node, and destination socket number, but for simplicity the example code in this article uses SAP to obtain this information. The Waiting Game With NetBIOS, you can choose to allow most NetBIOS commands to complete before returning control to the application, but most IPX/SPX commands return control immediately. In other words, most IPX/SPX commands are "no-wait" commands; there is no IPX/SPX "wait" counterpart. Since most NetBIOS developers use the "no-wait" variants, this difference should not pose a problem, but if you need to use a "wait," you can code it very simply by issuing the command and then looping on the in use field. Asynchronous Events IPX/SPX also has a feature that is not used with NetBIOS: the asynchronous event. An asynchronous event can be initiated at any time and, as the name implies, can be set to occur independent of an application's execution path. An event could be set up, for example, to automatically broadcast an IPX packet every 45 seconds. The application initiating this event could then continue processing and leave the timing and broadcasting of packets to the IPX event handler. The Network Control Block & the Event Control Block From a developer's perspective, the "core" of NetBIOS is the Network Control Block (NCB). IPX and SPX are based on an Event Control Block (ECB) and an IPX/SPX header. Figure 1 describes the fields in the ECB. ********************************************************* Figure 1: The IPX/SPX Event Control Block [- C structure -] void far *linkAddress Set by IPX void (far *ESRAddress)() Equivalent to NetBIOS POST routine BYTE inUseFlag Set when the ECB is in use, zero when it is available BYTE completionCode Equivalent to NetBIOS Command Completion WORD socketNumber Socket number associated with ECB BYTE IPXWorkspace[4] Set by IPX BYTE driverWorkspace[12] Set by IPX BYTE immediateAddress[6] Node address of next "hop" WORD fragmentCount Number of buffer fragments in packet ECBFragment fragmentDescriptor[2] Address and size of fragment(s) END of FIGURE 1 ********************************************************* [- ********************************************************* Figure 1a: The IPX/SPX Event Control Block (Pascal syntax) linkAddress :Pointer Set by IPX ESRAddress :Pointer Equivalent to NetBIOS POST routine InUseFlag :Byte; Set when the ECB is in use, zero when it is available CompletionCode :Byte; Equivalent to NetBIOS Command Completion SocketNumber :Word; Socket number associated with ECB IPXWorkspace :array[1..4] of byte; Set by IPX DriverWorkspace :array[1..12] of byte; Set by IPX ImmediateAddress:array[1..6] of byte; (Tnodeaddress) Node address of next "hop" FragmentCount :word; Number of buffer fragments in packet Fragment :array[1.. ] of Tfragment Address and size of fragment(s) (Note: this structure is declared as the Tecb type in the nwIPX unit) END of FIGURE 1a ********************************************************* -] Note that the ECB contains a field that has no equivalent in the NCB called the immediate address field. This field should be populated with the node address of the first "hop" on the way to the packet's ultimate destination. Novell provides an API call to populate this field, the IPXGetLocalTarget() API available in the NetWare Client SDK. IPX Send Example The sample code in this article includes simple examples written under DOS with the NetWare Client SDK. Figure 2 shows a routine sending an IPX packet. ********************************************************* Figure 2: IPX Send [- C example -] /* Send "Hello!" to the station at network 0x11111111, node 0x222222222222, socket 0x3333 using IPX */ void IPXSayHello() { char buffer[] = "Hello!"; ECB ecb; IPXHeader header; int transTime; header.packetType = 4; memset(header.destination.network, 0x11, 4); memset(header.destination.node, 0x22, 6); memset(header.destination.socket, 0x33, 2); ecb.ESRAddress = NULL; ecb.socketNumber = 0x4444; IPXGetLocalTarget(header.destination, ecb.immediateAddress, &transTime); ecb.fragmentCount = 2; ecb.fragmentDescriptor[0].address = &header; ecb.fragmentDescriptor[0].size = sizeof(IPXHeader); ecb.fragmentDescriptor[1].address = buffer; ecb.fragmentDescriptor[1].size = strlen(buffer) + 1; IPXSendPacket(&ecb); } END of FIGURE 2 ********************************************************* [- ********************************************************* Figure 2a: IPX Send (Pascal example) { Send "Hello!" to the station at network $11111111, node $222222222222, socket $3333 using IPX } Procedure IPXSayHello; Var buffer:string; ecb:Tecb; header:TipxHeader; transTime:Word; begin Buffer:="Hello!"; header.packetType := 4; FillChar(header.destination.network,4,$11); FillChar(header.destination.node, 6,$22); FillChar(header.destination.socket, 2,$33); ecb.ESRAddress:=NIL; ecb.socketNumber:=$4444; IPXGetLocalTarget(header.destination, ecb.immediateAddress, transTime); ecb.fragmentCount:=2; ecb.fragment[1].address:= @header; ecb.fragment[1].size := SizeOf(TIPXHeader); ecb.fragment[2].address:= @buffer[1]; ecb.fragment[2].size:= ord(buffer[0]); IPXSendPacket(ecb); end; END of FIGURE 2a ********************************************************* -] The first apparent difference between IPX and NetBIOS is that IPX uses two buffers where NetBIOS would use one. The first buffer is the IPX Header containing the source and destination addresses, the packet type, and several "housekeeping" fields. Refer to Figure 3 for a description of the IPX header. ********************************************************* Figure 3: IPX Header WORD checkSum Included to conform to Xerox IDP standard Set to FFFF by IPX WORD length Length of entire IPX packet including header Set by IPX BYTE transportControl Hop count - Set to zero by IPX BYTE packetType IPX packet type is 4 IPXAddress destination Address the packet is sent to [- Pascal: of type TInternetworkAddress -] IPXAddress source Address of node sending packet set by IPX [- Pascal: of type TinternetworkAddress -] END of FIGURE 3 ********************************************************* The second buffer is the data to be sent. Two fields in the IPX header must be set for an IPX send: the packet type and the destination address. IPX packets are type 4, SPX packets are type 5. [- Note that according to the original xerox definitions this statement is not correct. Type 4 packets are reserved for the PEP protocol. Use type 0 (undefined) when transmitting standard IPX packets-] The destination address consists of a four-byte network number, a six-byte node number, and a two-byte socket number. If these examples used an Event Service Routine (ESR), the ESR address would be filled with the address of a procedure to be run when the send completes, but since NULL is specified, this routine will not be run. The ESR is equivalent to the NetBIOS POST routine. When the IPX send executes, the rest of the fields in the IPX header are filled in automatically, including the source address. You must specify the socket number to be included in the source address, but the socket need not be open to send a packet. For this example, socket number 0x4444 was arbitrarily chosen. The immediate address field described above must be filled in as well, and the IPXGetLocalTarget() API call fills in this field with the appropriate value. It is passed the final destination of the packet and it calculates the address of the "first hop" on the way to the final destination. Note that if the target workstation is on the same subnet as the sending workstation the immediate address will be the same as the final destination. Otherwise, it will be a bridge or router on the subnet. Each of the buffers sent in the IPX packet is considered to be a fragment. Since there are two buffers (the IPX header and the data), the fragment count is equal to two. The address and size of the fragments are then entered, starting with the IPX header. As soon as all of the relevant fields are filled, the example calls IPXSendPacket() and passes it the address of the ECB. Receiving an IPX packet is much like sending one from a programming standpoint, except that you do not need to set the IPX header fields. In the ECB, you should set the ESR address, socket number, immediate address, and fragment descriptors. Note about socket numbers: the socket number specified for an IPX send does not need to be open, but for an IPX receive the socket must be open. The API call to receive an IPX packet is IPXListenForPacket(). SPX Connection Example Figure 4 contains a code sample that establishes an SPX connection. Before the request for an SPX connection is submitted, several ECBs are already listening for data (this is important). SPX temporarily "steals" two ECBs from the available and waiting ones for connection maintenance, and then it puts the stolen ECBs back in the pool when finished. If there are no pending ECBs for SPX to use, it cannot send an acknowledgement to the remote site and the connection will stall and time out. ********************************************************* Figure 4: Establishing an SPX Connection [- C code example -] /* Start an SPX connection with the station at network 0x11111111, node 0x222222222222, socket 0x3333, use local socket 0x4444 */ #define NUM_BUFFS 5 void call() { ECB send, receive[NUM_BUFFS], connect, term; SPXHeader sendHdr, rcvHdr[NUM_BUFFS], connHdr; char buffer[NUM_BUFFS][80], sendbuf[] = "Hello!"; int i, ccode, packetsReceived; WORD spxConnectionID; for (i = 0; i < NUM_BUFFS; i++) { receive[i].ESRAddress = NULL; receive[i].socketNumber = 0x4444; receive[i].fragmentCount = 2; receive[i].fragmentDescriptor[0].address = &(rcvHdr[i]); receive[i].fragmentDescriptor[0].size = sizeof(SPXHeader); receive[i].fragmentDescriptor[1].address = &(buffer[i]); receive[i].fragmentDescriptor[1].size = 80; SPXListenForSequencedPacket(receive[i]); } connect.ESRAddress = NULL; connect.socketNumber = 0x4444; connect.fragmentCount = 1; connect.fragmentDescriptor[0].address = &connHdr; connect.fragmentDescriptor[0].size = sizeof(SPXHeader); memset(connHdr.destination.network, 0x11, 4); memset(connHdr.destination.node, 0x22, 6); memset(connHdr.destination.socket, 0x33, 2); ccode = SPXEstablishConnection(0, 0, &spxConnectionID, &connect); printf("SPXEstablishConnection return code = 0x%x\n", ccode); if (ccode != 0) return; while (connect.inUseFlag != 0) IPXRelinquishControl(); if (connect.completionCode != 0) return; send.ESRAddress = NULL; send.fragmentCount = 2; send.fragmentDescriptor[0].address = &sendHdr; send.fragmentDescriptor[0].size = sizeof(SPXHeader); send.fragmentDescriptor[1].address = sendbuf; send.fragmentDescriptor[1].size = 7; SPXSendSequencedPacket(spxConnectionID, &send); packetsReceived = 0; while (packetsReceived < 10) { for (i = 0; i < NUM_BUFFS; i++) { if (receive[i].inUseFlag != 0) { if (receive[i].completionCode != 0) { packetsReceived = 10; /* If we get an error, terminate */ break; } printf("Received: %s\n", buffer[i]); packetsReceived++; } SPXListenForSequencedPacket(receive[i]); } IPXRelinquishControl(); } term.ESRAddress = NULL; term.fragmentCount = 1; term.fragmentDescriptor[0].address = &connHdr; term.fragmentDescriptor[0].size = sizeof(SPXHeader); SPXTerminateConnection(spxConnectionID, &term); while (term.inUseFlag != 0) IPXRelinquishControl(); for (i = 0; i < NUM_BUFFS; i++) IPXCancelEvent(receive[i]); } END of FIGURE 4 ********************************************************* [- ********************************************************* Figure 4a: Establishing an SPX Connection (Pascal example) { Start an SPX connection with the station at network $11111111, node $222222222222, socket $3333, use local socket $4444 } CONST NUM_BUFFS=5; Procedure call; Var send,connect,term:Tecb; receive :array[1..NUM_BUFFS] of Tecb; sendHdr, connHdr : TspxHeader; rcvHdr :array[1..NUM_BUFFS] of TspxHeader; buffer :array[1..NUM_BUFFS] of string[80]; sendBuf :string; i,packetsReceived:Integer; spxConnectionId :word; begin; sendbuf:="Hello!"; for i:= 1 to NUM_BUFFS do begin receive[i].ESRAddress := NIL; receive[i].socketNumber := $4444; receive[i].fragmentCount = 2; receive[i].fragment[1].address := @rcvHdr[i]; receive[i].fragment[1].size := sizeof(TSPXHeader); receive[i].fragment[2].address := @buffer[i]; receive[i].fragment[2].size := 80; SPXListenForSequencedPacket(receive[i]); end; connect.ESRAddress := NIL; connect.socketNumber := $4444; connect.fragmentCount := 1; connect.fragment[1].address := @connHdr; connect.fragment[1].size := sizeof(TSPXHeader); FillChar(connHdr.destination.network, 4, $11); FillChar(connHdr.destination.node, 6, $22); FillChar(connHdr.destination.socket, 2, $33); IF NOT SPXEstablishConnection(0, 0, spxConnectionID, connect) then begin writeln('SPXEstablishConnection return code', HexStr(nwSpx.result,2)); exit; end; while (connect.inUseFlag <> 0) do IPXRelinquishControl(); if (connect.completionCode <> 0) then exit; send.ESRAddress := NIL; send.fragmentCount := 2; send.fragment[1].address = @sendHdr; send.fragment[1].size := sizeof(TSPXHeader); send.fragment[2].address := @sendbuf[0]; send.fragment[2].size := ord(sendBuf[0])+1; SPXSendSequencedPacket(spxConnectionID, send); packetsReceived := 0; while (packetsReceived < 10) do begin for i :=1 to NUM_BUFFS do begin if (receive[i].inUseFlag <> 0) and (receive[i].completionCode <> 0) then begin packetsReceived := 10; exit; { If we get an error, terminate } end; writeln('Received: ", buffer[i]); inc(packetsReceived); SPXListenForSequencedPacket(receive[i]); end; IPXRelinquishControl; end; term.ESRAddress := NIL; term.fragmentCount := 1; term.fragment[1].address := @connHdr; term.fragment[1].size := sizeof(TSPXHeader); SPXTerminateConnection(spxConnectionID, term); while (term.inUseFlag <> 0) do IPXRelinquishControl; for i:=1 to NUM_BUFFS do IPXCancelEvent(receive[i]); end; END of FIGURE 4a ********************************************************* -] This process may sound complicated, but everything happens transparently. As long as there are extra ECBs available, the application never knows they have been borrowed, since SPX puts them back in the exact same state they were in when they were pressed into service. If the connection is established with the SPX watchdog enabled, the watchdog monitors the connection and notifies the application if the connection fails, even if the application is not currently sending data over the connection. This feature is useful for applications that start SPX connections, but use them infrequently. For simplicity, however, the example does not use the SPX watchdog. After the listen ECBs have been posted, the connection ECB is then set up in much the same way the IPX send ECB was, except that this ECB has only one fragment: the SPX header. The destination network, node, and socket also are set the same way they were in the previous example. SPXEstablishConnection() is passed a retry count of zero, indicating that you should use the default value for number of retries. This value is set in the workstation's NET.CFG file using the IPX RETRY COUNT parameter, which defaults to 20. The last zero passed in SPXEstablishConnection() indicates not to use the SPX watchdog. The SPX connection ID is returned as the third parameter. The SPX connection ID can be considered equivalent to the NetBIOS local session number. Next, the sample code attempts to establish a connection. It polls the ECB's in use flag waiting for the event to complete. The IPXRelinquishControl() call is very important at this stage. If the code did nothing but sit in a tight loop, IPX and SPX would never get the chance to do any processing. IPXRelinquishControl() allows the IPX/SPX layer to get some work done. Once the in use flag is set to zero, the example checks the return code to see if the attempt to establish a connection was successful. The code does not illustrate how to handle the various failure cases, but the most likely cause of a failure would be that the other side is not yet listening for a connection, just like in NetBIOS. After establishing the connection, packets can be sent to the remote station. The SPXSendSequencedPacket() call requires much less information than its IPX counterpart. Since the connection is already established, all SPXSendSequencedPacket() needs is the SPX connection ID, an ESR address, and the fragment information. After sending a packet, the example program waits for ten packets to arrive. When an ECB comes back, the example displays the data and then re-submits the ECB so that it can be used to receive a packet again. After receiving ten packets, it issues an SPXTerminateConnection() call to notify the other side that it is done. The call to terminate the connection takes almost the same parameters that the establish connection call does, except that there is no need to fill out any information in the SPX header. Once the connection has been terminated, the pending listen ECBs must be cancelled. To do so, the example calls IPXCancelEvent(). Unlike most other ECB-related calls, IPXCancelEvent() does not return until the ECB has been cancelled so there is no need to poll the in use flag. Event Service Routines Event Service Routines (ESRs) serve the same purpose as the NetBIOS POST routines, but require a little more setup than the standard POST routine. Most ESRs are written in Assembly, although some call C functions. Figure 5 shows a generic ESR that calls a C function after allocating its own stack. This is very important since the amount of free stack space (if any) at interrupt time is unknown, and any attempt by a C function to use the stack could result in memory corruption if the stack is overflowed. The only way to guarantee that this will not occur is to allocate sufficient stack space in the ESR. ********************************************************* Figure 5: Example Event Service Routine (ESR) [- C/ASM code -] .MODEL LARGE public _ReceiveESRHandler extrn _ProcessReceiveData:PROC .DATA ; The stack segment and pointer must be saved so that you can set up ; your own stack. stk_seg dw 0 ; variable to store old stack segment stk_ptr dw 0 ; variable to store old stack pointer stk_stk dw 512 dup (0) ; new stack of 1024 bytes in length stk_end dw 0 ; the end of the stack .CODE ; @datasize is TRUE if the model is MEDIUM or LARGE and FALSE if the ; model is SMALL or COMPACT. Just modify the .MODEL ???? above for the ; model you want. ES/SI holds the seg/offset of the currently used ECB ; that ProcessReceivedData needs to process. _ReceiveESRHandler PROC far mov ax,DGroup mov ds,ax mov stk_seg,ss ; Save the stack segment mov stk_ptr,sp ; Save the stack pointer mov ss,ax ; move the segment of new_stk into ss mov sp,offset stk_end ; move offset of new_stk to sp IF @datasize push es ; push es if mem. model medium/large ENDIF push si call _ProcessReceivedData mov ss,stk_seg ; Restore old stack segment mov sp,stk_ptr ; Restore old stack pointer retf _ReceiveESRHandler ENDP END END of FIGURE 5 ********************************************************* [- ********************************************************* Figure 5a: Example Event Service Routine (ESR) (BASM/Pascal) { The stack segment and pointer must be saved so that you can set up your own stack. } Var stk_stk:array[1..512] of word; { new stack of 1024 bytes in length } stk_end:word; { the end of the stack } {$F+} Procedure ESRhandler(Var p:Tpecb); { * Type TPecb=^Tecb } begin . . end; {$F-} {$F+} Procedure ListenESR; assembler; asm { ES:SI are the only valid registers when entering this procedure ! } mov dx, seg stk_stk { = seg @DATA } mov ds, dx mov dx,ss { setup of a new local stack } mov bx,sp { ss:sp copied to dx:bx} mov ax,ds mov ss,ax mov sp,offset stk_end push dx { push old ss:sp on new stack } push bx push es { * push es:si on stack as local vars } push si { * } mov di,sp { * } push ss { * push address of local ptr on stack } push di { * } CALL EsrHandler add sp,4 { skip stack ptr-copy } pop bx { restore ss:sp from new stack } pop dx mov sp,bx mov ss,dx end; {$F-} Note that a local stack of 1024 bytes (512 words) may not be large enough for some applications calling other functions within the ESRhandler. Increase the stacksize by 1024 bytes at a time to determine the stack requirement. END END of FIGURE 5a ********************************************************* -] Figure 6 contains a code fragment demonstrating the use of an ESR. It receives ten SPX packets just like the example in Figure 3 does, but it uses an ESR instead of polling the in use flag. The assembly language routine from Figure 4 is declared as the ESR, and it in turn calls the C [-/Pascal-] function ProcessReceivedData(). ********************************************************* Figure 6: Using an Event Service Routine (ESR) [- C Code -] int packetCount = 0; void ProcessReceivedData(ECB *ecb) { packetCount++; printf("%s\n", ecb->fragmentDescriptor[1].address); SPXListenForSequencedPacket(ecb); /* Re-issue the listen */ } main() { . . /* This code is identical to SPX setup code in Fig. 4, except */ . /* for receive[i].ESRAddress line, which will be as follows: */ receive[i].ESRAddress = (void (far *) () ) ReceiveESRHandler; . . /* The send ECB does not normally use an ESR. */ . while (packetCount < 10) IPXRelinquishControl(); . . /* Shut down connection, cancel ECBs */ . } END of FIGURE 6 ********************************************************* [- ********************************************************* Figure 6a: Using an Event Service Routine (ESR) (Pascal) Var PacketCount; Procedure ProcessReceivedData(Var ECB:Tecb) begin inc(packetCount); writeln(string(ecb^.fragment[2].address^)); SPXListenForSequencedPacket(ecb); { Re-issue the listen } end; begin { main body } PacketCount:=0; . . { This code is identical to SPX setup code in Fig. 4a, except } . { for receive[i].ESRAddress line, which will be as follows: } receive[i].ESRAddress := @ReceiveESRHandler; . . { The send ECB does not normally use an ESR. } . while (packetCount < 10) do IPXRelinquishControl; . . { Shut down connection, cancel ECBs } . end; END of FIGURE 6a ********************************************************* -] IPX and SPX may look a little more complicated than NetBIOS at first, but as soon as you begin using these protocols, you see how similar they really are. Using IPX/SPX requires slightly more effort, but the performance and compatibility gains when running under NetWare more than compensate. If you are thinking about becoming more familiar with IPX and SPX development, feel free to contact Novell's Developer Support group at 1-800-NETWARE (1-800-638-9273) or 1-801-429-5588.